Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for procs and method calls in inclusion validation dropdown resolution #1040

Merged
merged 4 commits into from
Nov 15, 2024

Conversation

lavoiesl
Copy link
Contributor

@lavoiesl lavoiesl commented Jun 7, 2024

Add support for a few variations of inclusion constraints.

The motivating factor behind this is to be able to resolve to a list of classes that can only be resolved at runtime.

For example, this is not stable, it depends in which order the classes are loaded:

validates :model, inclusion: { in: ActiveRecord::Base.descendants.map(&:name) }

It needs a lambda:

validates :model, inclusion: { in: ->() { ActiveRecord::Base.descendants.map(&:name) } }

However, stateful lambda are prohibited, because they depend on the task instance:

validates :model, inclusion: { in: ->() { |task| task.some_method } }

While at it, this PR also adds support for bounded enumerables:

validates :timeout, inclusion: { in: (0..30).step(5) }

See the update test and comments for a full explanation.

@lavoiesl lavoiesl requested review from bony2023 and ianyau28 June 7, 2024 19:15
@lavoiesl lavoiesl marked this pull request as ready for review June 7, 2024 19:46
@lavoiesl lavoiesl requested a review from a team June 7, 2024 19:46
app/helpers/maintenance_tasks/tasks_helper.rb Outdated Show resolved Hide resolved
app/helpers/maintenance_tasks/tasks_helper.rb Outdated Show resolved Hide resolved
Copy link
Contributor

@bony2023 bony2023 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing, thank you! 🎉
Few comments but not a blocker to merge

app/helpers/maintenance_tasks/tasks_helper.rb Outdated Show resolved Hide resolved
test/dummy/app/tasks/maintenance/params_task.rb Outdated Show resolved Hide resolved
Copy link
Member

@etiennebarrie etiennebarrie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want this. We're doing a best effort job of showing possible values when the inclusion validator is simple enough, but we shouldn't go and call random code, this is called in a view after all.

We have a way to extend the form by adding a maintenance_tasks/tasks/custom partial in the application. (It's not documented BTW sorry). I guess you could use this to conditionally add fields, hide the previous fields for an attribute, if you wanted to go down that route.

I think it's problematic that some validations can now break your views or make choices inaccessible. There should be some kind of opt-in at the very least.

Or maybe it's just not something that should be done in the engine. We do mention "Normally maintenance tasks are ephemeral, so they are used briefly and then deleted." in the README. The initial use case of wanting to list the application records feels like something that might be built in the application directly instead in some kind of admin interface, if it needs to access any and all models at any time.

https://github.com/Shopify/maintenance_tasks#should-i-use-maintenance-tasks

Otherwise one can always do Rails.application.eager_load!; ActiveRecord::Base.descendants in a console and stick the result in the validation of the task's param (since the task is temporary, it should be fine). Or just live with the fact that the form will not include a dropdown for the model, the target user for the engine being a developer, they should be able to deal with that situation.

app/helpers/maintenance_tasks/tasks_helper.rb Outdated Show resolved Hide resolved
app/helpers/maintenance_tasks/tasks_helper.rb Outdated Show resolved Hide resolved
@lavoiesl
Copy link
Contributor Author

lavoiesl commented Jun 10, 2024

We're doing a best effort job of showing possible values when the inclusion validator is simple enough, but we shouldn't go and call random code, this is called in a view after all.

I don't understand this point. I wouldn't qualify this as random code, it's very explicitly the proc to generate the options and it should be fast and reliable, since it's meant to be used in validations.


We have a way to extend the form by adding a maintenance_tasks/tasks/custom partial in the application. (It's not documented BTW sorry). I guess you could use this to conditionally add fields, hide the previous fields for an attribute, if you wanted to go down that route.

This seems very convoluted. Using the partial is only really useful to add fields. How would it work to hide previous fields?

If we were to say that the standard way of customizing fields is through overriding, we should make it easy in the gem, by making a dedicated _field partial or introducing some override mechanism on the fields, like callbacks.


I think it's problematic that some validations can now break your views or make choices inaccessible.

I don't understand this point. What would break? Are you referring to when it has more than 1000 items? If so, I replied elsewhere.

There should be some kind of opt-in at the very least.

As a compromise, I would be ok with a MaintenanceTasks.max_inclusion_items config that defaults to 0 (disabled behaviour).


Or maybe it's just not something that should be done in the engine. We do mention "Normally maintenance tasks are ephemeral, so they are used briefly and then deleted." in the README. The initial use case of wanting to list the application records feels like something that might be built in the application directly instead in some kind of admin interface, if it needs to access any and all models at any time.

I would argue that just because the gem is not intended for permanent tasks isn't a reason to not support making that a viable option.


Otherwise one can always do Rails.application.eager_load!; ActiveRecord::Base.descendants in a console and stick the result in the validation of the task's param (since the task is temporary, it should be fine).

We have cases with ~100 classes to pick from. This quickly becomes noise, a maintenance nightmare, or an annoyance.

Or just live with the fact that the form will not include a dropdown for the model, the target user for the engine being a developer, they should be able to deal with that situation.

This increases the chances of errors.


👉🏼 I think my most important point is that ActiveModel is designed to support dynamic inclusion constraints, but this gem decided to opt-out of that support. I understand that stateful constraints can't be supported, but I don't see why we shouldn't support stateless lambdas.

@etiennebarrie
Copy link
Member

As a compromise, I would be ok with a MaintenanceTasks.max_inclusion_items config that defaults to 0 (disabled behaviour).

I think that would be good.

I would argue that just because the gem is not intended for permanent tasks isn't a reason to not support making that a viable option.

Yes because there are a thousand no's for every yes. ☺️
First, we added params because this gem replaced enqueuing jobs from migrations, and people were passing arguments to the jobs. And it's useful to be able to test your task on a reduced amount of data without having to redeploy. But then we added dropdowns because if it's easy to do and it makes it easier on the user, why not. And here we are, adding another tweak to that simple thing and adding an arbitrary limit that needs configuration (in part to prevent regressions).
I hope you can see how supporting out-of-scope features tends to creep up.

I think my most important point is that ActiveModel is designed to support dynamic inclusion constraints, but this gem decided to opt-out of that support.

We support it just fine, we're just not making it easy for the user by providing the potential values as a dropdown.

I understand that stateful constraints can't be supported, but I don't see why we shouldn't support stateless lambdas.

So what are stateful constraints by the way? Lambdas that take the record? Also in: :some_method? We do have the object handy (although it's only initialized with its defaults obviously):

inclusion_values = resolve_inclusion_value(form_builder.object.class, parameter_name)

I guess they raise more issues, but I wonder why we wouldn't support that as well at this point.


Do we need to deal with potential exceptions raised from the lambda? Before we were just getting a value from the task, now we're calling code, so I guess there's more potential for failure here.


We should also document the dropdown feature (and the scope of the support for views with this PR) in the README as well. I guess that could be an indicator that the feature is too complex if we need too many words to explain it. Before: "if you have an inclusion validator on your parameter, if the allowed values is a Array, it will be used to build a dropdown". Now we'll need to add some more words.

Also just realized that if there's an if: or unless: on a validation, we're not respecting that (and ignoring the validation altogether), and always forcing the values to be in the allowed list with the dropdown, when the validation might not apply.

Interested to hear what @adrianna-chang-shopify and @nvasilevski think. And anyone else actually.

@etiennebarrie etiennebarrie self-requested a review June 12, 2024 15:34
@lavoiesl
Copy link
Contributor Author

So what are stateful constraints by the way? Lambdas that take the record? Also in: :some_method? We do have the object handy (although it's only initialized with its defaults obviously).

I guess they raise more issues, but I wonder why we wouldn't support that as well at this point.

Yeah, validations are designed to run within the context of the object with the values at the right place, so adding that limitation seems fair to me.

If someone wants to call a method, they can do so in a proc:

validates :model, inclusion: { in: ->() { MyClass.allowed_model_values } }

Do we need to deal with potential exceptions raised from the lambda? Before we were just getting a value from the task, now we're calling code, so I guess there's more potential for failure here.

Indeed, but if the code is throwing an exception, I think it's perfectly reasonable to let it raise unhandled


We should also document the dropdown feature (and the scope of the support for views with this PR) in the README as well. I guess that could be an indicator that the feature is too complex if we need too many words to explain it. Before: "if you have an inclusion validator on your parameter, if the allowed values is a Array, it will be used to build a dropdown". Now we'll need to add some more words.

Yeah, I can do that once we settle 👍🏼


Also just realized that if there's an if: or unless: on a validation, we're not respecting that (and ignoring the validation altogether), and always forcing the values to be in the allowed list with the dropdown, when the validation might not apply.

That's a valid concern, but it's also already true today. I don't think this PR makes the status quo any different.

@github-actions github-actions bot added the stale label Aug 12, 2024
@github-actions github-actions bot closed this Aug 20, 2024
@lavoiesl lavoiesl reopened this Oct 15, 2024
@github-actions github-actions bot removed the stale label Oct 16, 2024
Copy link
Contributor

@adrianna-chang-shopify adrianna-chang-shopify left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I somehow missed this when it was initially opened and just spotted it today 😅

Overall, I'm fine with this feature being added. I think it's well scoped and don't feel it encourages or discourages non-ephemeral tasks more than what we already offer in the gem 🤷‍♀️ IMO if we're confident we can resolve the proc / enumerable to a set of values to populate the dropdown, there's no harm in doing so.

I'd like to see MAX_INCLUSION_ITEMS made configurable, but I disagree with it defaulting to 0, since this will fully disable the dropdown support we already implemented with arrays, no?

app/helpers/maintenance_tasks/tasks_helper.rb Outdated Show resolved Hide resolved
@lavoiesl
Copy link
Contributor Author

I'd like to see MAX_INCLUSION_ITEMS made configurable, but I disagree with it defaulting to 0

Done

@lavoiesl
Copy link
Contributor Author

lavoiesl commented Nov 11, 2024

The tests are now failing, but I don't understand why. I don't see what it had to do with my PR 🤔

I rebased. Perhaps I picked up something?

@nvasilevski
Copy link
Contributor

Perhaps I picked up something?

It's reproducible on main so not related to the PR

dev.yml Outdated Show resolved Hide resolved
app/helpers/maintenance_tasks/tasks_helper.rb Outdated Show resolved Hide resolved
app/helpers/maintenance_tasks/tasks_helper.rb Outdated Show resolved Hide resolved
Copy link
Contributor

@adrianna-chang-shopify adrianna-chang-shopify left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks largely good to 🚢 on my end, other than the two small comments. Let's also add more docs to the README before this is merged. Thanks!

app/helpers/maintenance_tasks/tasks_helper.rb Outdated Show resolved Hide resolved
lib/maintenance_tasks.rb Outdated Show resolved Hide resolved
@lavoiesl
Copy link
Contributor Author

Let's also add more docs to the README before this is merged

There doesn't seem to be any section in the README that exists where this can fit.

I suggest we merge this and tackle documentation separately.

@lavoiesl lavoiesl dismissed etiennebarrie’s stale review November 12, 2024 20:42

Adrianna has taken over the review

Copy link
Member

@etiennebarrie etiennebarrie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think with the benefit of hindsight, I can more clearly see what needs to be done here. The original use case of having a list of models is something that's worth pursuing, however the generalization over all Enumerable doesn't feel useful to me (the developer can just call to_a in the validation definition: validates_inclusion_of :my_attribute, in: some_enum.to_a), and the necessity to add a new config option just sucks. We can trust the developer user as long as it's well documented.

And once we support calling a Proc, we might as well support the symbol/method call case, I feel like it's a very common refactoring to go from validates_inclusion_of :my_attribute, in: -> { ApplicationRecord.descendants } to

  validates_inclusion_of :my_attribute, in: :allowed_records

  def allowed_records
    ApplicationRecord.descendants
  end

I suggest we merge this and tackle documentation separately.

Let's not do this.

test/dummy/app/tasks/maintenance/params_task.rb Outdated Show resolved Hide resolved
app/helpers/maintenance_tasks/tasks_helper.rb Outdated Show resolved Hide resolved
@adrianna-chang-shopify adrianna-chang-shopify changed the title Add support for bounded enumerables and stateless Procs in inclusion validation Add support for procs and method calls in inclusion validation dropdown resolution Nov 14, 2024
test/dummy/app/tasks/maintenance/params_task.rb Outdated Show resolved Hide resolved
app/helpers/maintenance_tasks/tasks_helper.rb Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
@adrianna-chang-shopify
Copy link
Contributor

@etiennebarrie fixed to support arity-one procs / lambdas appropriately, and I also tweaked this to support callable objects, since that is supported in the Validations::ResolveValue module you linked.

README.md Outdated Show resolved Hide resolved
@adrianna-chang-shopify adrianna-chang-shopify merged commit 2f533ce into main Nov 15, 2024
31 checks passed
@adrianna-chang-shopify adrianna-chang-shopify deleted the seb-in-proc branch November 15, 2024 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants